A deep dive into CSS Scroll-Driven Animations. Learn to control easing and interpolation with `animation-timeline` for superior, performant custom scroll effects.
Beyond Smooth: Mastering Custom Scroll Animation Curves in CSS
For years, web developers have sought to control the one interaction that defines the web: scrolling. The introduction of scroll-behavior: smooth; was a monumental step forward, transforming jarring page jumps into a graceful glide. However, this one-size-fits-all solution lacks a crucial element for creative and user-centric design: control. The browser's default easing curve is fixed, offering no room for brand expression, nuanced user feedback, or unique interactive storytelling.
What if you could define the precise physics of your scroll? Imagine a scroll that starts slowly, accelerates rapidly, and then gently settles into place. Or a playful, bouncy effect for a creative portfolio. This level of granular control over scroll interpolation—the animation curve that dictates the speed of a scroll over its duration—has historically been the domain of complex, performance-intensive JavaScript libraries.
That era is ending. With the advent of the CSS Scroll-Driven Animations specification, developers now have native, performant tools to orchestrate animations based on scroll progress. This guide will take you on a deep dive into this new frontier, focusing on how to use properties like animation-timeline to craft custom scroll animation curves, moving far beyond the binary choice of 'auto' or 'smooth'.
A Quick Refresher: The Age of `scroll-behavior: smooth`
Before we explore the future, let's appreciate the past. The scroll-behavior property is a simple yet powerful CSS rule that dictates the behavior of scrolling when triggered by navigation, such as clicking an anchor link.
Its application is straightforward:
html {
scroll-behavior: smooth;
}
With this single line, any in-page navigation (e.g., clicking <a href="#section2">) will smoothly animate the viewport to the target element instead of instantly jumping. This was a massive win for user experience (UX), providing spatial context and a less disorienting journey through a webpage.
The Inherent Limitation
The primary drawback of scroll-behavior: smooth; is its inflexibility. The animation's duration and easing curve are predetermined by the browser vendor. There is no CSS property to make it faster, slower, or to apply a custom timing function like cubic-bezier(). This means every smooth scroll on every website feels largely the same—a reliable but uninspired experience.
The New Paradigm: CSS Scroll-Driven Animations
The CSS Scroll-Driven Animations specification fundamentally changes our relationship with scrolling. Instead of simply triggering a predefined animation, it allows us to link the progress of an animation directly to the progress of a scroll container. This means an animation can be 0% complete when a user is at the top of a page and 100% complete when they've scrolled to the bottom.
This is achieved through new CSS properties, primarily animation-timeline. This property tells an animation to derive its timing not from a clock (the default behavior) but from the position of a scrollbar.
There are two primary timelines you can use:
scroll(): Links an animation to the scroll progress of a container element. As the element scrolls, the animation progresses.view(): Links an animation to the progress of a specific element as it moves through the viewport. This is incredibly powerful for effects like revealing elements as they appear on screen.
For the purpose of creating a custom "feel" for a page's entire scroll experience, we will focus heavily on these new tools. They allow us to create effects that feel like custom scroll interpolation, even though we are technically animating other properties in sync with the scroll.
Unlocking Custom Curves: The Role of `animation-timing-function`
Here is the key insight: while animation-timeline links the scrollbar to the animation's progress, the animation-timing-function property is what allows us to define a custom interpolation curve!
Normally, animation-timing-function applies over a duration in seconds. In a scroll-driven animation, it applies over the duration of the scroll timeline. This means the easing curve we define will dictate how the animated property changes as the user scrolls.
Let's illustrate with a simple example: a scroll progress bar.
Example 1: A Progress Bar with Custom Easing
A linear progress bar is a common use case. But we can make it feel more dynamic with a custom curve.
HTML Structure
<div id="progress-bar"></div>
<main>
<!-- Your page content goes here -->
</main>
CSS Implementation
/* Basic styling for the progress bar */
#progress-bar {
position: fixed;
top: 0;
left: 0;
height: 8px;
background-color: #007BFF;
width: 100%;
/* Initially, it's scaled to 0 on the X-axis */
transform-origin: 0 50%;
transform: scaleX(0);
}
/* The animation definition */
@keyframes grow-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
/* The magic that links it all together */
#progress-bar {
/* Apply the animation */
animation: grow-progress linear;
/* Link the animation to the document's scroll timeline */
animation-timeline: scroll(root block);
/*
THIS IS THE CUSTOM CURVE!
Instead of linear, let's try an ease-out curve.
The progress will be fast at the beginning and slow down at the end.
*/
animation-timing-function: cubic-bezier(0, 0, 0.4, 1.1);
}
Breaking It Down
@keyframes grow-progress: We define a standard animation that scales an element from 0 to 1 on the X-axis.animation: grow-progress linear;: We apply this animation. The `linear` keyword here is just a placeholder; it will be overridden by our more specific `animation-timing-function`.animation-timeline: scroll(root block);: This is the core of the scroll-driven mechanic. It tells the `grow-progress` animation not to run on a timer but to follow the scrollbar of the root document (`root`) on its vertical axis (`block`).animation-timing-function: cubic-bezier(...): This is where we define our custom interpolation. Instead of the progress bar growing linearly with the scroll, it will now follow the velocity defined by our cubic-bezier curve. It will grow quickly at the start of the scroll and slow down as the user reaches the end of the page. This subtle change can make the interaction feel much more polished and responsive.
Crafting Complex Experiences: `view()` Timeline and Parallax
The `view()` timeline is even more powerful. It tracks an element as it passes through the visible viewport. This is perfect for creating entrance animations, parallax effects, and other interactions that depend on an element's visibility.
Let's create a non-linear parallax effect where different layers of an image move at different speeds, each with its own custom easing curve.
Example 2: Parallax with Unique Interpolation
HTML Structure
<div class="parallax-container">
<img src="foreground.png" class="parallax-layer foreground" alt="Foreground element">
<img src="midground.png" class="parallax-layer midground" alt="Midground element">
<img src="background.png" class="parallax-layer background" alt="Background element">
<h2 class="parallax-title">Scroll to Discover</h2>
</div>
CSS Implementation
.parallax-container {
position: relative;
height: 100vh;
overflow: hidden; /* Important for containing the layers */
}
.parallax-layer {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
}
/* Define a common keyframe for movement */
@keyframes move-up {
from { transform: translateY(0); }
to { transform: translateY(-100px); }
}
/* Apply animations with different curves and ranges */
.foreground {
animation: move-up linear;
animation-timeline: view(); /* Tracks this element's journey through the viewport */
animation-range: entry 0% exit 100%;
/* Aggressive ease-in: starts moving slowly, then very fast */
animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.335);
transform: translateY(50px); /* Initial offset */
}
.midground {
animation: move-up linear;
animation-timeline: view();
animation-range: entry 0% exit 100%;
/* A classic ease-in-out curve */
animation-timing-function: cubic-bezier(0.42, 0, 0.58, 1);
transform: translateY(20px); /* Smaller initial offset */
}
.background {
/* This layer will move very little or not at all to create depth */
}
.parallax-title {
animation: move-up linear;
animation-timeline: view();
animation-range: entry 0% exit 100%;
/* A bouncy, overshooting curve for expressive text */
animation-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
transform: translateY(0);
}
Dissecting the Parallax Effect
animation-timeline: view();: Each layer's animation is tied to its own visibility within the viewport.animation-range: This property defines the start and end points of the animation within the view timeline. `entry 0% exit 100%` means the animation starts when the element begins to enter the viewport and ends when it has completely exited.- Distinct `animation-timing-function`s: This is the key. The foreground moves with a fast, aggressive curve. The midground moves with a standard, smooth curve. The title has a playful bounce. Because each layer has a different interpolation curve, the resulting parallax effect is rich, dynamic, and far more engaging than a linear-speed effect.
Performance Considerations: The Compositor is Your Friend
One of the most significant advantages of CSS Scroll-Driven Animations over JavaScript-based solutions is performance. Most modern browsers can offload animations of specific properties—namely transform and opacity—to a separate process called the compositor thread.
This is a game-changer because:
- It's Non-Blocking: The main thread, which handles JavaScript, layout, and painting, is not involved. This means even if your site is running heavy scripts, your scroll animations will remain buttery smooth.
- It's Efficient: The compositor is highly optimized for moving bitmaps of content around the screen, leading to lower CPU/GPU usage and better battery life on mobile devices.
To ensure optimal performance, stick to animating transform (translate, scale, rotate) and opacity whenever possible. Animating properties that affect layout, like width, height, or margin, will force the browser back to the main thread, potentially causing jank and negating the performance benefits.
Browser Support and Progressive Enhancement
As of late 2023, CSS Scroll-Driven Animations are supported in Chromium-based browsers (Google Chrome, Microsoft Edge) starting around version 115. Support in Firefox and Safari is in active development and can often be enabled via experimental flags.
Given the mixed support, it's crucial to implement these features using progressive enhancement. The @supports at-rule is your best friend here.
/* Default styles for all browsers */
.reveal-on-scroll {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal-on-scroll.is-visible {
/* Fallback class toggled by JavaScript (e.g., with IntersectionObserver) */
opacity: 1;
transform: translateY(0);
}
/* Enhanced experience for supporting browsers */
@supports (animation-timeline: view()) {
.reveal-on-scroll {
/* Reset initial state for the animation */
opacity: 1;
transform: translateY(0);
/* Define the scroll-driven animation */
animation: fade-in-up linear;
animation-timeline: view();
animation-range: entry 10% entry 40%;
}
@keyframes fade-in-up {
from { opacity: 0; transform: translateY(50px); }
to { opacity: 1; transform: translateY(0); }
}
/* We no longer need the JS-driven class */
.reveal-on-scroll.is-visible {
opacity: 1; /* Or whatever the final state should be */
}
}
In this example, older browsers will get a perfectly acceptable fade-in effect managed by a small amount of JavaScript. Modern, supporting browsers will get the super-performant, scroll-linked CSS version, no JavaScript required for the animation itself.
Accessibility is Non-Negotiable: `prefers-reduced-motion`
With great power comes great responsibility. Complex and rapid animations can be disorienting or even physically harmful to users with vestibular disorders, causing dizziness, nausea, and headaches.
It is absolutely essential to respect the user's preference for reduced motion. The prefers-reduced-motion media query allows us to do this.
Always wrap your scroll-driven animations in this media query:
@media (prefers-reduced-motion: no-preference) {
.parallax-layer, .progress-bar, .reveal-on-scroll {
/* All your scroll-driven animation rules go here */
animation-timeline: view();
/* etc. */
}
}
When a user has enabled a "reduce motion" setting in their operating system, the animations inside this media query will not be applied. The site will remain perfectly functional but without the potentially problematic motion effects. This is a simple and profoundly important step for creating inclusive and accessible web experiences.
Conclusion: The Dawn of a New Era in Web Interaction
The ability to define custom animation curves tied to scrolling is more than a novelty; it's a fundamental shift in how we can design and build for the web. We are moving from a world of rigid, predefined scroll behaviors to one of expressive, performant, and art-directed interactions.
By mastering animation-timeline, view(), and animation-timing-function, you can:
- Enhance User Experience: Create intuitive and informative transitions that guide the user through your content.
- Improve Performance: Replace heavy JavaScript libraries with native CSS for smoother, more efficient animations.
- Boost Brand Expression: Infuse your website's interactions with a personality that reflects your brand identity.
- Build Responsibly: Use progressive enhancement and accessibility best practices to ensure a great experience for all users, on all devices.
The web is no longer just a document to be read; it's a space to be experienced. Dive in, experiment with different cubic-bezier() curves, and start crafting scroll experiences that are not just smooth, but truly memorable.